from io import BytesIO, StringIO
import struct
import json
import numpy
import re
import time

TUO_VISUAL_VERSION = 1


def getIdentityMatrix():
    return [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]

IdentityMatrix = (1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)
def checkIdentityMatrix(matrix):
   return matrix.A == IdentityMatrix

def getArrayFromMatrix(matrix):
    return [matrix.A11, matrix.A21, matrix.A31, matrix.A41, matrix.A12, matrix.A22, matrix.A32, matrix.A42, matrix.A13, matrix.A23, matrix.A33, matrix.A43, matrix.A14, matrix.A24, matrix.A34, matrix.A44]

class TuoVisual:

    def __init__(self, viewObjects, filename, info, nameReplace):
        self.data = TuoVisualData(filename)
        self.nameReplace = nameReplace
        self.viewObjects = viewObjects
        self.scene_min = info['scene_min']
        self.scene_max = info['scene_max']
        self.parse()

    def getRealName(self,name):
        if(self.nameReplace):
            r = re.compile('.+\$')
            res = r.search(name)
            if not res:
                return name
            else:
                mainName = name[res.start():res.end() - 1]
                realName = self.nameReplace.getBackReplaced(mainName)
                if not realName:
                    return name
                else:
                    subName = name[res.end():]
                    return realName + subName
        else:
            return name

    def parse(self):
        self.groups = []
        self.objects = []
        def add(obj, parentIndex = None):
            if len(obj.children) > 0:
                # --- Assembly
                curParentIndex = len(self.groups)

                curGroup = {}
                # print('checking Indentity Matrix')
                if not (checkIdentityMatrix(obj.localMatrix)):
                    curGroup['matrix'] = getArrayFromMatrix(obj.localMatrix)
                if(parentIndex):
                    curGroup['parent'] = parentIndex
                curGroup['name'] = self.getRealName(obj.name)
                curGroup['originInStructure'] = obj.getOriginInStructure()
                self.groups.append(curGroup)
                for child in obj.children:
                    # Recursively add children
                    #print('process a ViewObject1', child.inStructure)
                    add(child, curParentIndex)

                #if(obj.merged):
                #    for m in obj.merged:
                #        self.add_part(m, curParentIndex)
            elif hasattr(obj.obj, 'Group'):
                return
            else:
                # --- Part
                #print('add_part', obj.inStructure)
                self.add_part(obj, parentIndex)

        # print('checking Indentity Matrix')
        for obj in self.viewObjects:
            #print('process a viewObject', obj.getOriginInStructure())
            add(obj)

        self.groupLength = len(self.groups) #pure hierarchical group
        #sort objects by their volume
        def volumeSort(e):
            return e['volume']
        self.objects.sort(key = volumeSort, reverse=True)
        entityObjects = []
        for obj in self.objects:
            entityGroup = {}
            curIndex = len(self.groups)
            if('parent' in obj):
                #print('obj:', obj)
                parentGroup = self.groups[obj['parent']]
                if('sub' not in parentGroup):
                    parentGroup['sub'] = [curIndex]
                else:
                    parentGroup['sub'].append(curIndex)
                entityGroup['parent'] = obj['parent']
            entityGroup['name'] = obj['name']
            entityGroup['level'] = obj['level']
            entityGroup['originInStructure'] = obj['originInStructure']
            entityGroup['matrix'] = obj['matrix']
            #if('centers' in obj):
            #    entityGroup['centers'] = obj['centers']
            #    entityGroup['distances'] = obj['distances']
            self.groups.append(entityGroup)
            if('face' in obj):
                faceObject = {}
                faceObject['parent'] = curIndex
                faceObject['data'] = obj['face']['data']
                faceObject['offset'] = obj['face']['offset']
                faceObject['type'] = 'f'
                entityObjects.append(faceObject)
            if('line' in obj):
                lineObject = {}
                lineObject['parent'] = curIndex
                lineObject['data'] = obj['line']['data']
                lineObject['offset'] = obj['line']['offset']
                lineObject['type'] = 'l'
                entityObjects.append(lineObject)
            if('point' in obj):
                pointObject = {}
                pointObject['parent'] = curIndex
                pointObject['data'] = obj['point']['data']
                pointObject['offset'] = obj['point']['offset']
                pointObject['type'] = 'p'
                entityObjects.append(pointObject)
        #add entity to groups
        for obj in entityObjects:
            curIndex = len(self.groups)
            if('parent' in obj):
                parentGroup = self.groups[obj['parent']]
                if('sub' not in parentGroup):
                    parentGroup['sub'] = [curIndex]
                else:
                    parentGroup['sub'].append(curIndex)
            self.groups.append(obj)
        
        self.tuoVisual = {}
        self.tuoVisual['info'] = {'groups': self.groupLength, \
'totals': len(self.groups), 'box': [self.scene_min, self.scene_max], 'version': TUO_VISUAL_VERSION}
        for i in range(len(self.groups)):
            self.tuoVisual[str(i)] = self.groups[i]
            
        blob = StringIO()
        blob.write(json.dumps(self.tuoVisual))
        blob.seek(0)
        self.tuoVisual = blob


    def add_part(self, obj, parentIndex):

        """
            to compute a object: {
                face: {
                    data: url,
                    offset: integer,
                },
                line: {
                    data: url,
                    offset: integer,
                },
                point: {
                    data: url,
                    offset: integer,
                },
                centers, #optional
                distances, #optional
                level,
                volume,
                name,
                parent #can be undefined
            }
        """
        tess = obj.tess
        wireDis = obj.wireDis
        # print('obj.name11:', obj.name, hasattr(obj.obj, 'Group') )
        points = obj.points
        geom = obj.geom
        vetsGroup = None
        indicesGroup = None
        colors = None
        indices = None
        vertices = None
        normals = None
        colorIndices = None
        if tess:
            (vertices, normals, indices, vetsGroup, indicesGroup, colors, colorIndices) = tess
        
        #if(len(vertices) < 1):
        #    return
        

        #print('colors:', colors)
        #first, we compute the volume of thie object
        #transformedBox = obj.obj.Shape.BoundBox.transformed(obj.parentMatrix)
        #volume = transformedBox.XLength * transformedBox.YLength * transformedBox.ZLength
        #then, we update the scene_min and scene_max
        #self.scene_min = _list3_min(
        #    self.scene_min,
        #    (transformedBox.XMin, transformedBox.YMin, transformedBox.ZMin)
        #)

        #self.scene_max = _list3_max(
        #    self.scene_max,
        #    (transformedBox.XMax, transformedBox.YMax, transformedBox.ZMax)
        #)

        #get face data
        dataUrl = None
        offset = None
        #(dataUrl, offset) = self.data.writeFaces(obj['face']['vertices'], obj['face']['normals'],\
        #    obj['face']['indices'], obj['face']['verticesGroup'], obj['face']['indicesGroup'], \
        #    obj['face']['colors'], obj['face']['colorsGroup'])


        (dataUrl, offset) = self.data.writeFaces(vertices, normals,\
            indices, vetsGroup, indicesGroup, \
            colors, colorIndices, geom[0], geom[1])


        faceData = dataUrl
        faceOffset = offset
        #get line data 
        #(dataUrl, offset) = self.data.writeWires(obj['line']['vertices'], obj['line']['verticesGroup'])
        (dataUrl, offset) = self.data.writeWires(obj.wireDis, obj.wireGroup, geom[2], geom[3])
        wireData = dataUrl
        wireOffset = offset
        #get point data
        #(dataUrl, offset) = self.data.writePoints(obj['point']['vertices'])
        (dataUrl, offset) = self.data.writePoints(obj.points)
        pointData = dataUrl
        pointOffset = offset
        result = {}
        result['face'] = {'data':faceData,'offset':faceOffset}
        result['line'] = {'data':wireData,'offset':wireOffset}
        result['point'] = {'data':pointData,'offset':pointOffset}
        result['volume'] = obj.volume
        result['level'] = obj.level
        result['name']  = self.getRealName(obj.name)
        result['originInStructure'] = obj.getOriginInStructure()
        result['matrix'] = getArrayFromMatrix(obj.localMatrix)
        #if(obj['level'] == 2 or obj['level'] == 1):
        #result['centers'] =[[c.x, c.y, c.z] for c in obj['centers']]
        #result['distances'] = obj['distances']


        #for n in obj['names']:
        #    result['name'].append(self.getRealName(n))
        if(isinstance(parentIndex, int)):
            result['parent'] = parentIndex
        self.objects.append(result)


class TuoVisualData:
    EXPECT_FILE_SIZE = 1024*1024 # 128Kbytes

    defaultColor = [round(val / 255., 4) for val in (244,244,244)] + [1.0]
    def __init__(self, fileName):
        print('TuoVisualData init')
        self.blobs = {}
        self.curBlob = None
        #self.curIndex = 0        
        self.curBlobName = None

        self.blobCount = 0
        self.baseName = fileName

    def createNewBlob(self):

        self.curBlob = BytesIO()
        versionCode = b''
        versionCode += struct.pack('<I', TUO_VISUAL_VERSION)
        self.curBlob.write(versionCode)
        #self.curIndex = 0
        self.curBlobName = self.baseName + '.' + str(self.blobCount) + '.bin'
        self.blobs[self.curBlobName] = self.curBlob
        self.blobCount += 1

    def createWorkBlob(self):
        if not (self.curBlob):
            self.createNewBlob()
            resultDataUrl = self.curBlobName
            resultOffset = self.curBlob.tell()
        else:
            curBlobLen = self.curBlob.tell()
            if(curBlobLen > self.EXPECT_FILE_SIZE):
                self.createNewBlob()
                resultDataUrl = self.curBlobName
                resultOffset = self.curBlob.tell()
            else:
                resultOffset = self.curBlob.tell()
                resultDataUrl = self.curBlobName


        return (resultDataUrl, resultOffset)


    #return (dataUrl, offset)
    def writeBlob(self, blob):
        resultDataUrl = None
        resultOffset = None
        if not (self.curBlob):
            self.createNewBlob()
            resultDataUrl = self.curBlobName
            resultOffset = self.curBlob.tell()
            self.curBlob.write(blob.getbuffer())
        else:
            objBlobLen = blob.tell()
            if(objBlobLen > self.EXPECT_FILE_SIZE):
                oldBlob = self.curBlob
                oldBlobName = self.curBlobName
                self.createNewBlob()
                resultOffset = self.curBlob.tell()
                #self.blobs[self.curBlobName] = blob
                resultDataUrl = self.curBlobName
                self.curBlob.write(blob.getbuffer())
                self.blobs[self.curBlobName] = self.curBlob
                self.curBlob = oldBlob
                self.curBlobName = oldBlobName
            else:
                resultOffset = self.curBlob.tell()
                resultDataUrl = self.curBlobName
                self.curBlob.write(blob.getbuffer())

        curBlobLen = self.curBlob.tell()
        if(curBlobLen > self.EXPECT_FILE_SIZE):
            self.createNewBlob()

        return (resultDataUrl, resultOffset)


    def writeFaces(self, vertices, normals, indices, verticesGroup,\
indicesGroup, colors, colorIndices, faceType, faceMeta):
        blob = BytesIO()
        # info = self.createWorkBlob()
        # blob = self.curBlob


        vetsCountOffset = 0
        indicesCountOffset = 0


        verticesBin = createVerticesBinary(vertices)
        normalsBin = createVerticesBinary(normals)
        indicesBin = createIndicesBinary(indices)
        #    if(colors and len(colors) > 1):
        #        curColor = colors[i]
        #    elif(colors):
        #        curColor = colors[0]
        #    else:
        #        curColor = self.defaultColor
        #if not colors:
        #    colors = [self.defaultColor]
        colorsBin = createColorsBinary(colors)
        colorIndicesBin = createGroupsBinary(colorIndices)
        verticesGroupBin = createGroupsBinary(verticesGroup)
        groupsBin = createGroupsBinary(indicesGroup)
        faceTypeBin = createUint8Binary(faceType)
        faceMetaBin = createFloatBinary(faceMeta)
        #first write vertices, normals,indices,and,colors
        #for i in range(len(vetsGroup)):
        #    vetsCount = vetsGroup[i]
        #    indicesCount = indicesGroup[i]
        #    curVets = vertices[vetsCountOffset: vetsCountOffset + vetsCount]
        #    curNors = normals[vetsCountOffset: vetsCountOffset + vetsCount]
        #    curIndices = indices[indicesCountOffset: (indicesCountOffset + indicesCount)]
        #    if(colors and len(colors) > 1):
        #        curColor = colors[i]
        #    elif(colors):
        #        curColor = colors[0]
        #    else:
        #        curColor = self.defaultColor
        #    self.writeFace(curVets, curNors, curIndices, curColor, blob) 
        #    vetsCountOffset += vetsCount
        #    indicesCountOffset += indicesCount
        blob.write(verticesBin)
        blob.write(normalsBin)
        blob.write(indicesBin)
        blob.write(colorIndicesBin)
        blob.write(colorsBin)
        blob.write(groupsBin)
        blob.write(verticesGroupBin)
        blob.write(faceTypeBin)
        blob.write(faceMetaBin)

        return self.writeBlob(blob)
        # return info

    def writeFace(self, vertices, normals, indices, color, blob):
        verticesBin = createVerticesBinary(vertices)
        normalsBin = createVerticesBinary(normals)
        indicesBin = createIndicesBinary(indices)
        colorBin = createColorBinary(color)
        blob.write(verticesBin)
        blob.write(normalsBin)
        blob.write(indicesBin)
        blob.write(colorBin)

    def writeWires(self, wireDis, verticesGroup, edgeType, edgeMeta):
        blob = BytesIO()
        # info = self.createWorkBlob()
        # blob = self.curBlob

        verticesBin = createVerticesBinary(wireDis)
        groupsBin = createGroupsBinary(verticesGroup)
        edgeTypeBin = createUint8Binary(edgeType)
        edgeMetaBin = createFloatBinary(edgeMeta)

        blob.write(verticesBin)
        blob.write(groupsBin)
        blob.write(edgeTypeBin)
        blob.write(edgeMetaBin)
        return self.writeBlob(blob)
        # return info

    def writeWire(self, wire, blob):
        verticesBin = createVerticesBinary(wire)
        blob.write(verticesBin)

    def writePoints(self, points):
        blob = BytesIO()
        # info = self.createWorkBlob()
        # blob = self.curBlob

        verticesBin = createVerticesBinary(points)
        blob.write(verticesBin)

        return self.writeBlob(blob)
        # return info


def getUnsignedIntBinary(count):
    return struct.pack('<I', count)


#vertices: [Vector(x,y,z), ...]
def createVerticesBinary(vertices):
    result = b''
    count = len(vertices)
    if(count > 4294967295):
        raise ValueError('too many vertices')

    result += getUnsignedIntBinary(count)

    toWrite = []
    for vet in vertices:
        toWrite.append(vet.x)
        toWrite.append(vet.y)
        toWrite.append(vet.z)

    result += struct.pack('<%sf' % len(toWrite), *toWrite)

    return result


def createColorsBinary(colors):
    result = b''
    count = len(colors)
    if(count > 4294967295):
        raise ValueError('too many colors')
    result += getUnsignedIntBinary(count)
    toWrite = []
    for color in colors:
        # result += struct.pack('<f', color[0]) + struct.pack('<f', color[1]) + struct.pack('<f', color[2]) + struct.pack('<f', color[3])
        toWrite.extend(color)
    result += struct.pack('<%sf' % len(toWrite), *toWrite)
    return result


#indices: [(i1,i2,i3), ...]
def createIndicesBinary(indices):
    count = len(indices)
    if(count > 4294967295):
        raise ValueError('too many indices')
    if(len(indices) == 0):
        maxIndex = 0
    else:
        maxIndex = numpy.matrix(indices).max()
    dataType = 1 # 1,2,4
    formatType = 'B'
    if(maxIndex > 0xffff):
        dataType = 4
        formatType = '<I'
    elif(maxIndex > 0xff):
        dataType = 2
        formatType = '<H'
    else:
        dataType = 1
        formatType = 'B'
    result = b''
    result += struct.pack('B', dataType)
    result += getUnsignedIntBinary(count)

    toWrite = []
    for index in indices:
        # result += struct.pack(formatType, index[0]) + struct.pack(formatType, index[1]) + struct.pack(formatType, index[2])
        toWrite.extend(index)


    if(formatType == 'B'):
        tmp = '%sB'
    elif(formatType == '<I'):
        tmp = '<%sI'
    else:
        tmp = '<%sH'

    result += struct.pack(tmp % len(toWrite), *toWrite)
    return result

#data: [d1,d2,d3, ...]
def createUint8Binary(datas):
    count = len(datas)
    if(count > 4294967295):
        raise ValueError('too many datas')
    result = b''
    result += getUnsignedIntBinary(count)
    toWrite = []
    for data in datas:
        # result += struct.pack('B', data)
        toWrite.append(data)
    result += struct.pack('%sB' % len(toWrite), *toWrite)
    return result

#data: [d1,d2,d3, ...]
def createFloatBinary(datas):
    result = b''
    count = len(datas)
    if(count > 4294967295):
        raise ValueError('too many datas')
    result += getUnsignedIntBinary(count)
    toWrite = []
    for data in datas:
        # result += struct.pack('<f', data)
        toWrite.append(data)
    result += struct.pack('<%sf' % len(toWrite), *toWrite)
    return result

#indices: [g0, g1, g2, ...]
def createGroupsBinary(groups):
    count = len(groups)
    if(count > 4294967295):
        raise ValueError('too many groups')
    if(len(groups) == 0):
        maxGroup = 0
    else:
        maxGroup = numpy.matrix(groups).max()
    dataType = 1 # 1,2,4
    formatType = 'B'
    if(maxGroup > 0xffff):
        dataType = 4
        formatType = '<I'
    elif(maxGroup > 0xff):
        dataType = 2
        formatType = '<H'
    else:
        dataType = 1
        formatType = 'B'
    result = b''
    result += struct.pack('B', dataType)
    result += getUnsignedIntBinary(count)
    toWrite = []
    for g in groups:
        # result += struct.pack(formatType, g)
        toWrite.append(g)

    if(formatType == 'B'):
        tmp = '%sB'
    elif(formatType == '<I'):
        tmp = '<%sI'
    else:
        tmp = '<%sH'
    result += struct.pack(tmp % len(toWrite), *toWrite)
    return result

